/*
 * Created on 2004-11-23
 *
 */
package org.hibernate.cfg;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.DuplicateMappingException;
import org.hibernate.FetchMode;
import org.hibernate.MappingException;
import org.hibernate.cfg.reveng.AssociationInfo;
import org.hibernate.cfg.reveng.DatabaseCollector;
import org.hibernate.cfg.reveng.JDBCReader;
import org.hibernate.cfg.reveng.JDBCToHibernateTypeHelper;
import org.hibernate.cfg.reveng.MappingsDatabaseCollector2;
import org.hibernate.cfg.reveng.ReverseEngineeringStrategy;
import org.hibernate.cfg.reveng.RootClassEx;
import org.hibernate.cfg.reveng.TableIdentifier;
import org.hibernate.connection.ConnectionProvider;
import org.hibernate.engine.Mapping;
import org.hibernate.engine.Versioning;
import org.hibernate.mapping.Collection;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.DependantValue;
import org.hibernate.mapping.ForeignKey;
import org.hibernate.mapping.KeyValue;
import org.hibernate.mapping.ManyToOne;
import org.hibernate.mapping.MetaAttribute;
import org.hibernate.mapping.OneToMany;
import org.hibernate.mapping.OneToOne;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.RootClass;
import org.hibernate.mapping.SimpleValue;
import org.hibernate.mapping.Table;
import org.hibernate.mapping.ToOne;
import org.hibernate.mapping.Value;
import org.hibernate.type.ForeignKeyDirection;
import org.hibernate.type.Type;
import org.hibernate.type.TypeFactory;
import org.hibernate.util.JoinedIterator;
import org.hibernate.util.StringHelper;

/**
 * @author max
 * 
 */
public class JDBCBinder {

	private Settings settings;
	private ConnectionProvider connectionProvider;
	private static final Log log = LogFactory.getLog(JDBCBinder.class);

	private final Mappings mappings;

	private final JDBCMetaDataConfiguration cfg;
	private ReverseEngineeringStrategy revengStrategy;

	/**
	 * @param mappings
	 * @param configuration
	 */
	public JDBCBinder(JDBCMetaDataConfiguration cfg, Settings settings, Mappings mappings, ReverseEngineeringStrategy revengStrategy) {
		this.cfg = cfg;
		this.settings = settings;
		this.mappings = mappings;
		this.revengStrategy = revengStrategy;
	}

	/**
	 * 反转数据库并解析为实体类（返回反转信息）；
	 */
	public DatabaseCollector readFromDatabase(String catalog, String schema, Mapping mapping) {

		this.connectionProvider = settings.getConnectionProvider();

		try {
			DatabaseCollector collector = readDatabaseSchema(catalog, schema);
			createPersistentClasses(collector, mapping); // move this to a
														// different step!
			return collector;
		} catch (SQLException e) {
			throw settings.getSQLExceptionConverter().convert(e, "Reading from database", null);
		} finally {
			if (connectionProvider != null)
				connectionProvider.close();
		}

	}

	/**
	 * Read JDBC Metadata from the database. Does not create any classes or
	 * other ORM releated structures.
	 * 
	 * @param catalog
	 * @param schema
	 * @return
	 * @throws SQLException
	 */
	public DatabaseCollector readDatabaseSchema(String catalog, String schema) throws SQLException {
		// use default from settings if nothing else specified.
		catalog = catalog != null ? catalog : settings.getDefaultCatalogName();
		schema = schema != null ? schema : settings.getDefaultSchemaName();

		JDBCReader reader = JDBCReaderFactory.newJDBCReader(cfg.getProperties(), settings, revengStrategy);
		DatabaseCollector dbs = new MappingsDatabaseCollector2(mappings, reader.getMetaDataDialect());
		reader.readDatabaseSchema(dbs, catalog, schema);
		return dbs;
	}

	/**
	 * @param manyToOneCandidates
	 * @param mappings2
	 */
	private void createPersistentClasses(DatabaseCollector collector, Mapping mapping) {
		Map manyToOneCandidates = collector.getOneToManyCandidates();

		for (Iterator iter = mappings.iterateTables(); iter.hasNext();) {
			Table table = (Table) iter.next();
			if (table.getColumnSpan() == 0) {
				log.warn("Cannot create persistent class for " + table + " as no columns were found.");
				continue;
			}
			// TODO: this naively just create an entity per table
			// should have an opt-out option to mark some as helper tables,
			// subclasses etc.
			/*
			 * if(table.getPrimaryKey()==null ||
			 * table.getPrimaryKey().getColumnSpan()==0) {
			 * log.warn("Cannot create persistent class for " + table +
			 * " as no primary key was found."); continue; // TODO: just create
			 * one big embedded composite id instead. }
			 */

			if (revengStrategy.isManyToManyTable(table)) {
				log.debug("Ignoring " + table + " as class since rev.eng. says it is a many-to-many");
				continue;
			}
			
			RootClassEx rc = new RootClassEx();
			TableIdentifier tableIdentifier = TableIdentifier.create(table);
			String className = revengStrategy.tableToClassName(tableIdentifier);
			log.debug("Building entity " + className + " based on " + tableIdentifier);
			rc.setEntityName(className);
			rc.setClassName(className);
			rc.setProxyInterfaceName(rc.getEntityName()); // TODO: configurable
															// ?
			rc.setLazy(true);
			rc.setMetaAttributes(safeMeta(revengStrategy.tableToMetaAttributes(tableIdentifier)));
			rc.setDiscriminatorValue(rc.getEntityName());
			rc.setComment(table.getComment()); // 加入注释到实体类信息载体
			rc.setTable(table);
			try {
				mappings.addClass(rc);
			} catch (DuplicateMappingException dme) {
				// TODO: detect this and generate a "permutation" of it ?
				PersistentClass class1 = mappings.getClass(dme.getName());
				Table table2 = class1.getTable();
				throw new JDBCBinderException("Duplicate class name '" + rc.getEntityName() + "' generated for '" + table
						+ "'. Same name where generated for '" + table2 + "'");
			}
			mappings.addImport(rc.getEntityName(), rc.getEntityName());
			
			Set processed = new HashSet();

			PrimaryKeyInfo pki = bindPrimaryKeyToProperties(table, rc, processed, mapping, collector);
			bindColumnsToVersioning(table, rc, processed, mapping);
			bindOutgoingForeignKeys(table, rc, processed);
			bindColumnsToProperties(table, rc, processed, mapping);
			List incomingForeignKeys = (List) manyToOneCandidates.get(rc.getEntityName());
			bindIncomingForeignKeys(rc, processed, incomingForeignKeys, mapping);
			updatePrimaryKey(rc, pki);
		}
	}

	private void updatePrimaryKey(RootClass rc, PrimaryKeyInfo pki) {
		SimpleValue idValue = (SimpleValue) rc.getIdentifierProperty().getValue();

		Properties defaultStrategyProperties = new Properties();
		Property constrainedOneToOne = getConstrainedOneToOne(rc);
		if (constrainedOneToOne != null) {
			if (pki.suggestedStrategy == null) {
				idValue.setIdentifierGeneratorStrategy("foreign");
			}

			if (pki.suggestedProperties == null) {
				defaultStrategyProperties.setProperty("property", constrainedOneToOne.getName());
				idValue.setIdentifierGeneratorProperties(defaultStrategyProperties);
			}
		}

	}

	private Property getConstrainedOneToOne(RootClass rc) {
		Iterator propertyClosureIterator = rc.getPropertyClosureIterator();
		while (propertyClosureIterator.hasNext()) {
			Property property = (Property) propertyClosureIterator.next();
			if (property.getValue() instanceof OneToOne) {
				OneToOne oto = (OneToOne) property.getValue();
				if (oto.isConstrained()) {
					return property;
				}
			}
		}
		return null;
	}

	private Map safeMeta(Map map) {
		if (map == null) {
			return new HashMap();
		} else {
			return map;
		}
	}

	// bind collections.
	private void bindIncomingForeignKeys(PersistentClass rc, Set processed, List foreignKeys, Mapping mapping) {
		if (foreignKeys != null) {
			for (Iterator iter = foreignKeys.iterator(); iter.hasNext();) {
				ForeignKey foreignKey = (ForeignKey) iter.next();

				if (revengStrategy.excludeForeignKeyAsCollection(foreignKey.getName(),
						TableIdentifier.create(foreignKey.getTable()), foreignKey.getColumns(),
						TableIdentifier.create(foreignKey.getReferencedTable()), foreignKey.getReferencedColumns())) {
					log.debug("Rev.eng excluded one-to-many or one-to-one for foreignkey " + foreignKey.getName());
				} else if (revengStrategy.isOneToOne(foreignKey)) {
					Property property = bindOneToOne(rc, foreignKey.getTable(), foreignKey, processed, false, true);
					rc.addProperty(property);
				} else {
					Property property = bindOneToMany(rc, foreignKey, processed, mapping);
					rc.addProperty(property);
				}
			}
		}
	}

	private Property bindOneToOne(PersistentClass rc, Table targetTable, ForeignKey fk, Set processedColumns,
			boolean constrained, boolean inverseProperty) {

		OneToOne value = new OneToOne(targetTable, rc);
		value.setReferencedEntityName(revengStrategy.tableToClassName(TableIdentifier.create(targetTable)));

		boolean isUnique = isUniqueReference(fk);
		String propertyName = null;
		if (inverseProperty) {
			propertyName = revengStrategy.foreignKeyToInverseEntityName(fk.getName(),
					TableIdentifier.create(fk.getReferencedTable()), fk.getReferencedColumns(),
					TableIdentifier.create(targetTable), fk.getColumns(), isUnique);
		} else {
			propertyName = revengStrategy.foreignKeyToEntityName(fk.getName(),
					TableIdentifier.create(fk.getReferencedTable()), fk.getReferencedColumns(),
					TableIdentifier.create(targetTable), fk.getColumns(), isUnique);
		}

		Iterator columns = fk.getColumnIterator();
		while (columns.hasNext()) {
			Column fkcolumn = (Column) columns.next();
			checkColumn(fkcolumn);
			value.addColumn(fkcolumn);
			processedColumns.add(fkcolumn);
		}

		value.setFetchMode(FetchMode.SELECT);

		value.setConstrained(constrained);
		value.setForeignKeyType(constrained ? ForeignKeyDirection.FOREIGN_KEY_FROM_PARENT
				: ForeignKeyDirection.FOREIGN_KEY_TO_PARENT);

		return makeEntityProperty(propertyName, true, targetTable, fk, value, inverseProperty);
		// return makeProperty(TableIdentifier.create(targetTable),
		// propertyName, value,
		// true, true, value.getFetchMode() != FetchMode.JOIN, null, null);
	}

	/**
	 * @param mutable
	 * @param table
	 * @param fk
	 * @param columnsToBind
	 * @param processedColumns
	 * @param rc
	 * @param propName
	 */
	private Property bindManyToOne(String propertyName, boolean mutable, Table table, ForeignKey fk, Set processedColumns) {
		ManyToOne value = new ManyToOne(table);
		value.setReferencedEntityName(fk.getReferencedEntityName());
		Iterator columns = fk.getColumnIterator();
		while (columns.hasNext()) {
			Column fkcolumn = (Column) columns.next();
			checkColumn(fkcolumn);
			value.addColumn(fkcolumn);
			processedColumns.add(fkcolumn);
		}
		value.setFetchMode(FetchMode.SELECT);

		return makeEntityProperty(propertyName, mutable, table, fk, value, false);
	}

	private Property makeCollectionProperty(String propertyName, boolean mutable, Table table, ForeignKey fk,
			Collection value, boolean inverseProperty) {
		AssociationInfo fkei = inverseProperty ? revengStrategy.foreignKeyToInverseAssociationInfo(fk) : revengStrategy
				.foreignKeyToAssociationInfo(fk);

		String fetchMode = null;
		String cascade = null;
		boolean update = mutable;
		boolean insert = mutable;

		if (fkei != null) {
			cascade = fkei.getCascade();
			if (cascade == null)
				cascade = "all"; // To ensure collections cascade to be
									// compatible with Seam-gen and previous
									// behavior.
			if (fkei.getUpdate() != null) {
				update = fkei.getUpdate().booleanValue();
			}
			if (fkei.getInsert() != null) {
				insert = fkei.getInsert().booleanValue();
			}

			fetchMode = fkei.getFetch();

		}

		if (FetchMode.JOIN.toString().equalsIgnoreCase(fetchMode)) {
			value.setFetchMode(FetchMode.JOIN);
		} else if (FetchMode.SELECT.toString().equalsIgnoreCase(fetchMode)) {
			value.setFetchMode(FetchMode.SELECT);
		} else {
			value.setFetchMode(FetchMode.SELECT);
		}

		return makeProperty(TableIdentifier.create(table), propertyName, value, insert, update,
				value.getFetchMode() != FetchMode.JOIN, cascade, null);

	}

	private Property makeEntityProperty(String propertyName, boolean mutable, Table table, ForeignKey fk, ToOne value,
			boolean inverseProperty) {
		AssociationInfo fkei = inverseProperty ? revengStrategy.foreignKeyToInverseAssociationInfo(fk) : revengStrategy
				.foreignKeyToAssociationInfo(fk);

		String fetchMode = null;
		String cascade = null;
		boolean update = mutable;
		boolean insert = mutable;

		if (fkei != null) {
			cascade = fkei.getCascade();
			if (fkei.getUpdate() != null) {
				update = fkei.getUpdate().booleanValue();
			}
			if (fkei.getInsert() != null) {
				insert = fkei.getInsert().booleanValue();
			}

			fetchMode = fkei.getFetch();

		}

		if (FetchMode.JOIN.toString().equalsIgnoreCase(fetchMode)) {
			value.setFetchMode(FetchMode.JOIN);
		} else if (FetchMode.SELECT.toString().equalsIgnoreCase(fetchMode)) {
			value.setFetchMode(FetchMode.SELECT);
		} else {
			value.setFetchMode(FetchMode.SELECT);
		}

		return makeProperty(TableIdentifier.create(table), propertyName, value, insert, update,
				value.getFetchMode() != FetchMode.JOIN, cascade, null);
	}

	/**
	 * @param rc
	 * @param processed
	 * @param table
	 * @param object
	 */
	private Property bindOneToMany(PersistentClass rc, ForeignKey foreignKey, Set processed, Mapping mapping) {

		Table collectionTable = foreignKey.getTable();

		Collection collection = new org.hibernate.mapping.List(rc); // iPan alter.
//		Collection collection = new org.hibernate.mapping.Set(rc); // MASTER
																	// TODO:
																	// allow
																	// overriding
																	// collection
																	// type

		collection.setCollectionTable(collectionTable); // CHILD+

		boolean manyToMany = revengStrategy.isManyToManyTable(collectionTable);
		if (manyToMany) {
			// log.debug("Rev.eng said here is a many-to-many");
			// TODO: handle "the other side should influence the name"
		}

		if (manyToMany) {

			ManyToOne element = new ManyToOne(collection.getCollectionTable());
			// TODO: find the other foreignkey and choose the other side.
			Iterator foreignKeyIterator = foreignKey.getTable().getForeignKeyIterator();
			List keys = new ArrayList();
			while (foreignKeyIterator.hasNext()) {
				Object next = foreignKeyIterator.next();
				if (next != foreignKey) {
					keys.add(next);
				}
			}

			if (keys.size() > 1) {
				throw new JDBCBinderException("more than one other foreign key to choose from!"); // todo:
																									// handle
																									// better
																									// ?
			}

			ForeignKey fk = (ForeignKey) keys.get(0);

			String tableToClassName = bindCollection(rc, foreignKey, fk, collection);

			element.setReferencedEntityName(tableToClassName);
			element.addColumn(fk.getColumn(0));
			collection.setElement(element);

		} else {
			String tableToClassName = bindCollection(rc, foreignKey, null, collection);
			OneToMany oneToMany = new OneToMany(collection.getOwner());
			PersistentClass pc = new RootClass();
			pc.setClassName(Object.class.getName());
			oneToMany.setAssociatedClass(pc); // OneToMany是不能生成的，因为此时Many的类型还不知道；所以，这里直接赋值Object类型了；
			oneToMany.setReferencedEntityName(tableToClassName); // Child
			mappings.addSecondPass(new JDBCCollectionSecondPass(mappings, collection));

			collection.setElement(oneToMany);
		}
		// bind keyvalue
		KeyValue referencedKeyValue;
		String propRef = collection.getReferencedPropertyName();
		if (propRef == null) {
			referencedKeyValue = collection.getOwner().getIdentifier();
		} else {
			referencedKeyValue = (KeyValue) collection.getOwner().getProperty(propRef).getValue();
		}

		SimpleValue keyValue = new DependantValue(collectionTable, referencedKeyValue);
		// keyValue.setForeignKeyName("none"); // Avoid creating the foreignkey
		// key.setCascadeDeleteEnabled( "cascade".equals(
		// subnode.attributeValue("on-delete") ) );
		Iterator columnIterator = foreignKey.getColumnIterator();
		while (columnIterator.hasNext()) {
			Column fkcolumn = (Column) columnIterator.next();
			if (fkcolumn.getSqlTypeCode() != null) { // TODO: user defined
														// foreign ref columns
														// does not have a type
														// set.
				guessAndAlignType(collectionTable, fkcolumn, mapping, false); // needed
																				// to
																				// ensure
																				// foreign
																				// key
																				// columns
																				// has
																				// same
																				// type
																				// as
																				// the
																				// "property"
																				// column.
			}
			keyValue.addColumn(fkcolumn);
		}

		collection.setKey(keyValue);

		mappings.addCollection(collection);

		return makeCollectionProperty(StringHelper.unqualify(collection.getRole()), true, rc.getTable(), foreignKey,
				collection, true);
		// return makeProperty(TableIdentifier.create( rc.getTable() ),
		// StringHelper.unqualify( collection.getRole() ), collection, true,
		// true, true, "none", null); // TODO: cascade isn't all by default

	}

	private String bindCollection(PersistentClass rc, ForeignKey fromForeignKey, ForeignKey toForeignKey,
			Collection collection) {
		ForeignKey targetKey = fromForeignKey;
		String collectionRole = null;
		boolean collectionLazy = false;
		boolean collectionInverse = false;
		TableIdentifier foreignKeyTable = null;
		String tableToClassName;

		if (toForeignKey != null) {
			targetKey = toForeignKey;
		}

		boolean uniqueReference = isUniqueReference(targetKey); // TODO: need to
																// look one step
																// further for
																// many-to-many!
		foreignKeyTable = TableIdentifier.create(targetKey.getTable());
		TableIdentifier foreignKeyReferencedTable = TableIdentifier.create(targetKey.getReferencedTable());

		if (toForeignKey == null) {

			collectionRole = revengStrategy.foreignKeyToCollectionName(fromForeignKey.getName(), foreignKeyTable,
					fromForeignKey.getColumns(), foreignKeyReferencedTable, fromForeignKey.getReferencedColumns(),
					uniqueReference);

			tableToClassName = revengStrategy.tableToClassName(foreignKeyTable);
		} else {

			collectionRole = revengStrategy.foreignKeyToManyToManyName(fromForeignKey,
					TableIdentifier.create(fromForeignKey.getTable()), toForeignKey, uniqueReference);

			tableToClassName = revengStrategy.tableToClassName(foreignKeyReferencedTable);
		}

		collectionInverse = revengStrategy.isForeignKeyCollectionInverse(targetKey.getName(), foreignKeyTable,
				targetKey.getColumns(), foreignKeyReferencedTable, targetKey.getReferencedColumns());

		collectionLazy = revengStrategy.isForeignKeyCollectionLazy(targetKey.getName(), foreignKeyTable,
				targetKey.getColumns(), foreignKeyReferencedTable, targetKey.getReferencedColumns());

		collectionRole = makeUnique(rc, collectionRole);

		String fullRolePath = StringHelper.qualify(rc.getEntityName(), collectionRole);
		if (mappings.getCollection(fullRolePath) != null) {
			log.debug(fullRolePath + " found twice!");
		}

		collection.setRole(fullRolePath); // Master.setOfChildren+
		collection.setInverse(collectionInverse); // TODO: allow overriding this
		collection.setLazy(collectionLazy);
		collection.setFetchMode(FetchMode.SELECT);

		return tableToClassName;
	}

	/**
	 * return true if this foreignkey is the only reference from this table to
	 * the same foreign table
	 */
	private boolean isUniqueReference(ForeignKey foreignKey) {

		Iterator foreignKeyIterator = foreignKey.getTable().getForeignKeyIterator();
		while (foreignKeyIterator.hasNext()) {
			ForeignKey element = (ForeignKey) foreignKeyIterator.next();
			if (element != foreignKey && element.getReferencedTable().equals(foreignKey.getReferencedTable())) {
				return false;
			}
		}
		return true;
	}

	static private class PrimaryKeyInfo {

		String suggestedStrategy = null;
		Properties suggestedProperties = null;
	}

	private PrimaryKeyInfo bindPrimaryKeyToProperties(Table table, RootClass rc, Set processed, Mapping mapping,
			DatabaseCollector collector) {
		SimpleValue id = null;
		String idPropertyname = null;

		PrimaryKeyInfo pki = new PrimaryKeyInfo();

		List keyColumns = null;
		if (table.getPrimaryKey() != null) {
			keyColumns = table.getPrimaryKey().getColumns();
		} else {
			log.debug("No primary key found for " + table + ", using all properties as the identifier.");
			keyColumns = new ArrayList();
			Iterator iter = table.getColumnIterator();
			while (iter.hasNext()) {
				Column col = (Column) iter.next();
				keyColumns.add(col);
			}
		}

		final TableIdentifier tableIdentifier = TableIdentifier.create(table);

		String tableIdentifierStrategyName = "assigned";

		boolean naturalId;

		if (keyColumns.size() > 1) {
			log.debug("id strategy for " + rc.getEntityName() + " since it has a multiple column primary key");
			naturalId = true;

			id = handleCompositeKey(rc, processed, keyColumns, mapping);
			idPropertyname = revengStrategy.tableToIdentifierPropertyName(tableIdentifier);
			if (idPropertyname == null) {
				idPropertyname = "id";
			}
		} else {
			pki.suggestedStrategy = revengStrategy.getTableIdentifierStrategyName(tableIdentifier);
			String suggestedStrategy = pki.suggestedStrategy;
			if (suggestedStrategy == null) {
				suggestedStrategy = collector.getSuggestedIdentifierStrategy(tableIdentifier.getCatalog(),
						tableIdentifier.getSchema(), tableIdentifier.getName());
				if (suggestedStrategy == null) {
					suggestedStrategy = "assigned";
				}
				tableIdentifierStrategyName = suggestedStrategy;
			} else {
				tableIdentifierStrategyName = suggestedStrategy;
			}

			naturalId = "assigned".equals(tableIdentifierStrategyName);
			Column pkc = (Column) keyColumns.get(0);
			checkColumn(pkc);

			id = bindColumnToSimpleValue(table, pkc, mapping, !naturalId);

			idPropertyname = revengStrategy.tableToIdentifierPropertyName(tableIdentifier);
			if (idPropertyname == null) {
				idPropertyname = revengStrategy.columnToPropertyName(tableIdentifier, pkc.getName());
			}

			processed.add(pkc);
		}
		id.setIdentifierGeneratorStrategy(tableIdentifierStrategyName);
		pki.suggestedProperties = revengStrategy.getTableIdentifierProperties(tableIdentifier);
		id.setIdentifierGeneratorProperties(pki.suggestedProperties);
		if (naturalId) {
			id.setNullValue("undefined");
		}

		Property property = makeProperty(tableIdentifier, makeUnique(rc, idPropertyname), id, true, true, false, null, null);
		rc.setIdentifierProperty(property);
		rc.setIdentifier(id);

		return pki;
	}

	/**
	 * bind many-to-ones
	 * 
	 * @param table
	 * @param rc
	 * @param primaryKey
	 */
	private void bindOutgoingForeignKeys(Table table, RootClass rc, Set processedColumns) {

		// Iterate the outgoing foreign keys and create many-to-one's
		for (Iterator iterator = table.getForeignKeyIterator(); iterator.hasNext();) {
			ForeignKey foreignKey = (ForeignKey) iterator.next();

			boolean mutable = true;
			if (contains(foreignKey.getColumnIterator(), processedColumns)) {
				if (!cfg.preferBasicCompositeIds())
					continue; // it's in the pk, so skip this one
				mutable = false;
			}

			if (revengStrategy.excludeForeignKeyAsManytoOne(foreignKey.getName(),
					TableIdentifier.create(foreignKey.getTable()), foreignKey.getColumns(),
					TableIdentifier.create(foreignKey.getReferencedTable()), foreignKey.getReferencedColumns())) {
				// TODO: if many-to-one is excluded should the column be marked
				// as processed so it won't show up at all ?
				log.debug("Rev.eng excluded *-to-one for foreignkey " + foreignKey.getName());
			} else if (revengStrategy.isOneToOne(foreignKey)) {
				Property property = bindOneToOne(rc, foreignKey.getReferencedTable(), foreignKey, processedColumns, true,
						false);
				rc.addProperty(property);
			} else {
				boolean isUnique = isUniqueReference(foreignKey);
				String propertyName = revengStrategy
						.foreignKeyToEntityName(foreignKey.getName(), TableIdentifier.create(foreignKey.getTable()),
								foreignKey.getColumns(), TableIdentifier.create(foreignKey.getReferencedTable()),
								foreignKey.getReferencedColumns(), isUnique);

				Property property = bindManyToOne(makeUnique(rc, propertyName), mutable, table, foreignKey, processedColumns);

				rc.addProperty(property);
			}
		}
	}

	/**
	 * @param table
	 * @param rc
	 * @param primaryKey
	 */
	private void bindColumnsToProperties(Table table, RootClass rc, Set processedColumns, Mapping mapping) {

		for (Iterator iterator = table.getColumnIterator(); iterator.hasNext();) {
			Column column = (Column) iterator.next();
			if (!processedColumns.contains(column)) {
				checkColumn(column);

				String propertyName = revengStrategy.columnToPropertyName(TableIdentifier.create(table), column.getName());

				Property property = bindBasicProperty(makeUnique(rc, propertyName), table, column, processedColumns, mapping);

				rc.addProperty(property);
			}
		}
	}

	private void bindColumnsToVersioning(Table table, RootClass rc, Set processed, Mapping mapping) {
		TableIdentifier identifier = TableIdentifier.create(table);

		String optimisticLockColumnName = revengStrategy.getOptimisticLockColumnName(identifier);

		if (optimisticLockColumnName != null) {
			Column column = table.getColumn(new Column(optimisticLockColumnName));
			if (column == null) {
				log.warn("Column " + column + " wanted for <version>/<timestamp> not found in " + identifier);
			} else {
				bindVersionProperty(table, identifier, column, rc, processed, mapping);
			}
		} else {
			log.debug("Scanning " + identifier + " for <version>/<timestamp> columns.");
			Iterator columnIterator = table.getColumnIterator();
			while (columnIterator.hasNext()) {
				Column column = (Column) columnIterator.next();
				boolean useIt = revengStrategy.useColumnForOptimisticLock(identifier, column.getName());
				if (useIt && !processed.contains(column)) {
					bindVersionProperty(table, identifier, column, rc, processed, mapping);
					return;
				}
			}
			log.debug("No columns reported while scanning for <version>/<timestamp> columns in " + identifier);
		}
	}

	private void bindVersionProperty(Table table, TableIdentifier identifier, Column column, RootClass rc, Set processed,
			Mapping mapping) {

		processed.add(column);
		String propertyName = revengStrategy.columnToPropertyName(identifier, column.getName());
		Property property = bindBasicProperty(makeUnique(rc, propertyName), table, column, processed, mapping);
		rc.addProperty(property);
		rc.setVersion(property);
		rc.setOptimisticLockMode(Versioning.OPTIMISTIC_LOCK_VERSION);
		log.debug("Column " + column.getName() + " will be used for <version>/<timestamp> columns in " + identifier);

	}

	private Property bindBasicProperty(String propertyName, Table table, Column column, Set processedColumns, Mapping mapping) {

		SimpleValue value = bindColumnToSimpleValue(table, column, mapping, false);

		return makeProperty(TableIdentifier.create(table), propertyName, value, true, true, false, null, null);
	}

	private SimpleValue bindColumnToSimpleValue(Table table, Column column, Mapping mapping, boolean generatedIdentifier) {
		SimpleValue value = new SimpleValue(table);
		value.addColumn(column);
		value.setTypeName(guessAndAlignType(table, column, mapping, generatedIdentifier));
		return value;
	}

	/**
	 * @param columnIterator
	 * @param processedColumns
	 * @return
	 */
	private boolean contains(Iterator columnIterator, Set processedColumns) {
		while (columnIterator.hasNext()) {
			Column element = (Column) columnIterator.next();
			if (processedColumns.contains(element)) {
				return true;
			}
		}
		return false;
	}

	private void checkColumn(Column column) {
		if (column.getValue() != null) {
			// throw new
			// JDBCBinderException("Binding column twice should not happen. " +
			// column);
		}
	}

	/**
	 * @param column
	 * @param generatedIdentifier
	 * @return
	 */
	private String guessAndAlignType(Table table, Column column, Mapping mapping, boolean generatedIdentifier) {
		// TODO: this method mutates the column if the types does not
		// match...not good.
		// maybe we should copy the column instead before calling this method.
		Integer sqlTypeCode = column.getSqlTypeCode();
		String location = "Table: " + Table.qualify(table.getCatalog(), table.getSchema(), table.getQuotedName())
				+ " column: " + column.getQuotedName();
		if (sqlTypeCode == null) {
			throw new JDBCBinderException("sqltype is null for " + location);
		}

		String preferredHibernateType = revengStrategy.columnToHibernateTypeName(TableIdentifier.create(table),
				column.getName(), sqlTypeCode.intValue(), column.getLength(), column.getPrecision(), column.getScale(),
				column.isNullable(), generatedIdentifier);

		Type wantedType = TypeFactory.heuristicType(preferredHibernateType);

		if (wantedType != null) {
			int[] wantedSqlTypes = wantedType.sqlTypes(mapping);

			if (wantedSqlTypes.length > 1) {
				throw new JDBCBinderException("The type " + preferredHibernateType + " found on " + location
						+ " spans multiple columns. Only single column types allowed.");
			}

			int wantedSqlType = wantedSqlTypes[0];
			if (wantedSqlType != sqlTypeCode.intValue()) {
				log.debug("Sql type mismatch for " + location + " between DB and wanted hibernate type. Sql type set to "
						+ typeCodeName(sqlTypeCode.intValue()) + " instead of " + typeCodeName(wantedSqlType));
				column.setSqlTypeCode(new Integer(wantedSqlType));
			}
		} else {
			log.debug("No Hibernate type found for " + preferredHibernateType
					+ ". Most likely cause is a missing UserType class.");
		}

		if (preferredHibernateType == null) {
			throw new JDBCBinderException("Could not find javatype for " + typeCodeName(sqlTypeCode.intValue()));
		}

		return preferredHibernateType;
	}

	private String typeCodeName(int sqlTypeCode) {
		return sqlTypeCode + "(" + JDBCToHibernateTypeHelper.getJDBCTypeName(sqlTypeCode) + ")";
	}

	/**
	 * Basically create an [classname]Id.class and add properties for it.
	 * 
	 * @param rc
	 * @param compositeKeyColumns
	 * @param processed
	 * @return
	 */
	private SimpleValue handleCompositeKey(RootClass rc, Set processedColumns, List keyColumns, Mapping mapping) {
		Component pkc = new Component(rc);
		pkc.setMetaAttributes(Collections.EMPTY_MAP);
		pkc.setEmbedded(false);

		String compositeIdName = revengStrategy.tableToCompositeIdName(TableIdentifier.create(rc.getTable()));
		if (compositeIdName == null) {
			compositeIdName = revengStrategy.classNameToCompositeIdName(rc.getClassName());
		}
		pkc.setComponentClassName(compositeIdName);
		Table table = rc.getTable();
		List list = null;
		if (cfg.preferBasicCompositeIds()) {
			list = new ArrayList(keyColumns);
		} else {
			list = findForeignKeys(table.getForeignKeyIterator(), keyColumns);
		}
		for (Iterator iter = list.iterator(); iter.hasNext();) {
			Object element = iter.next();
			Property property;
			if (element instanceof Column) {
				Column column = (Column) element;
				if (processedColumns.contains(column)) {
					throw new JDBCBinderException("Binding column twice for primary key should not happen: " + column);
				} else {
					checkColumn(column);

					String propertyName = revengStrategy.columnToPropertyName(TableIdentifier.create(table),
							column.getName());
					property = bindBasicProperty(makeUnique(pkc, propertyName), table, column, processedColumns, mapping);

					processedColumns.add(column);
				}
			} else if (element instanceof ForeignKeyForColumns) {
				ForeignKeyForColumns fkfc = (ForeignKeyForColumns) element;
				ForeignKey foreignKey = fkfc.key;
				String propertyName = revengStrategy.foreignKeyToEntityName(foreignKey.getName(),
						TableIdentifier.create(foreignKey.getTable()), foreignKey.getColumns(),
						TableIdentifier.create(foreignKey.getReferencedTable()), foreignKey.getReferencedColumns(), true);
				property = bindManyToOne(makeUnique(pkc, propertyName), true, table, foreignKey, processedColumns);
				processedColumns.addAll(fkfc.columns);
			} else {
				throw new JDBCBinderException("unknown thing");
			}

			markAsUseInEquals(property);
			pkc.addProperty(property);

		}

		return pkc;
	}

	/**
	 * @param property
	 */
	private void markAsUseInEquals(Property property) {
		Map m = new HashMap();
		MetaAttribute ma = new MetaAttribute("use-in-equals");
		ma.addValue("true");
		m.put(ma.getName(), ma);
		property.setMetaAttributes(m);
	}

	/**
	 * @param foreignKeyIterator
	 * @param columns
	 * @return
	 */
	private List findForeignKeys(Iterator foreignKeyIterator, List pkColumns) {

		List tempList = new ArrayList();
		while (foreignKeyIterator.hasNext()) {
			tempList.add(foreignKeyIterator.next());
		}

		// Collections.reverse(tempList);

		List result = new ArrayList();
		Column myPkColumns[] = (Column[]) pkColumns.toArray(new Column[pkColumns.size()]);

		for (int i = 0; i < myPkColumns.length; i++) {

			boolean foundKey = false;
			foreignKeyIterator = tempList.iterator();
			while (foreignKeyIterator.hasNext()) {
				ForeignKey key = (ForeignKey) foreignKeyIterator.next();
				List matchingColumns = columnMatches(myPkColumns, i, key);
				if (matchingColumns != null) {
					result.add(new ForeignKeyForColumns(key, matchingColumns));
					i += matchingColumns.size() - 1;
					foreignKeyIterator.remove();
					foundKey = true;
					break;
				}
			}
			if (!foundKey) {
				result.add(myPkColumns[i]);
			}

		}

		return result;
	}

	private List columnMatches(Column[] myPkColumns, int offset, ForeignKey key) {

		if (key.getColumnSpan() > (myPkColumns.length - offset)) {
			return null; // not enough columns in the key
		}

		List columns = new ArrayList();
		for (int j = 0; j < key.getColumnSpan(); j++) {
			Column column = myPkColumns[j + offset];
			if (!column.equals(key.getColumn(j))) {
				return null;
			} else {
				columns.add(column);
			}
		}
		return columns.isEmpty() ? null : columns;
	}

	static class ForeignKeyForColumns {

		protected final List columns;
		protected final ForeignKey key;

		public ForeignKeyForColumns(ForeignKey key, List columns) {
			this.key = key;
			this.columns = columns;
		}
	}

	private Property makeProperty(TableIdentifier table, String propertyName, Value value, boolean insertable,
			boolean updatable, boolean lazy, String cascade, String propertyAccessorName) {
		log.debug("Building property " + propertyName);
		Property prop = new Property();
		prop.setName(propertyName);
		prop.setValue(value);
		prop.setInsertable(insertable);
		prop.setUpdateable(updatable);
		prop.setLazy(lazy);
		prop.setCascade(cascade == null ? "none" : cascade);
		prop.setPropertyAccessorName(propertyAccessorName == null ? "property" : propertyAccessorName);
		bindMeta(prop, table);

		return prop;
	}

	private Property bindMeta(Property property, TableIdentifier identifier) {
		Iterator columnIterator = property.getValue().getColumnIterator();
		while (columnIterator.hasNext()) {
			Column col = (Column) columnIterator.next();

			Map map = revengStrategy.columnToMetaAttributes(identifier, col.getName());
			if (map != null) { // TODO: merge from each column ?
				property.setMetaAttributes(map);
			}
		}

		return property;
	}

	/**
	 * @param pkc
	 * @param string
	 * @return
	 */
	private String makeUnique(Component clazz, String propertyName) {
		return makeUnique(clazz.getPropertyIterator(), propertyName);
	}

	private String makeUnique(PersistentClass clazz, String propertyName) {
		List list = new ArrayList();

		if (clazz.hasIdentifierProperty()) {
			list.add(clazz.getIdentifierProperty());
		}

		if (clazz.isVersioned()) {
			list.add(clazz.getVersion());
		}

		JoinedIterator iterator = new JoinedIterator(list.iterator(), clazz.getPropertyClosureIterator());
		return makeUnique(iterator, propertyName);
	}

	/**
	 * @param clazz
	 * @param propertyName
	 * @return
	 */
	private static String makeUnique(Iterator props, String originalPropertyName) {
		int cnt = 0;
		String propertyName = originalPropertyName;
		Set uniqueNames = new HashSet();

		while (props.hasNext()) {
			Property element = (Property) props.next();
			uniqueNames.add(element.getName());
		}

		while (uniqueNames.contains(propertyName)) {
			cnt++;
			propertyName = originalPropertyName + "_" + cnt;
		}

		return propertyName;
	}

	public static void bindCollectionSecondPass(Collection collection, java.util.Map persistentClasses, Mappings mappings,
			java.util.Map inheritedMetas) throws MappingException {

		if (collection.isOneToMany()) {
			OneToMany oneToMany = (OneToMany) collection.getElement();
			PersistentClass persistentClass = mappings.getClass(oneToMany.getReferencedEntityName());

			if (persistentClass == null)
				throw new MappingException("Association " + collection.getRole() + " references unmapped class: "
						+ oneToMany.getReferencedEntityName());

			oneToMany.setAssociatedClass(persistentClass); // Child
		}

	}

	static class JDBCCollectionSecondPass extends CollectionSecondPass {

		/**
		 * @param mappings
		 * @param coll
		 */
		JDBCCollectionSecondPass(Mappings mappings, Collection coll) {
			super(mappings, coll);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.hibernate.cfg.HbmBinder.SecondPass#secondPass(java.util.Map,
		 * java.util.Map)
		 */
		public void secondPass(Map persistentClasses, Map inheritedMetas) throws MappingException {
			JDBCBinder.bindCollectionSecondPass(collection, persistentClasses, mappings, inheritedMetas);
		}

		public void doSecondPass(Map persistentClasses) throws MappingException {
			Value element = collection.getElement();
			DependantValue dep = null;
			String oldFkName = null;
			if (element instanceof DependantValue) {
				dep = (DependantValue) element;
				oldFkName = dep.getForeignKeyName();
				dep.setForeignKeyName("none"); // Workaround to avoid
												// DependantValue to create
												// foreignkey just because
												// reference columns are not the
												// same + no need to create keys
												// already in the db!
			}
			super.doSecondPass(persistentClasses);
			if (dep != null) {
				dep.setForeignKeyName(oldFkName);
			}

		}
	}

}
